package org.nuonuo.sdk.client;

import org.nuonuo.sdk.callback.Callback;
import org.nuonuo.sdk.callback.NCallback;
import org.nuonuo.sdk.jackson.JsonMapper;
import org.nuonuo.sdk.model.base.*;
import org.nuonuo.sdk.model.req.BillingNewReq;
import org.nuonuo.sdk.model.req.InvoiceFastRedReq;
import org.nuonuo.sdk.model.req.InvoiceRedReq;
import org.nuonuo.sdk.model.req.InvoiceReq;
import org.nuonuo.sdk.model.res.AuthTokenRes;
import org.nuonuo.sdk.model.res.BillingNewRes;
import org.nuonuo.sdk.model.res.InvoiceRes;
import org.nuonuo.sdk.model.res.Response;
import org.nuonuo.sdk.servlet.Servlets;
import org.nuonuo.sdk.valid.Validator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.validation.ValidationException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @Version 1.0
 * @Author az
 * @Date 2024-04-01 13:40:56
 * @ClassName NClient
 * @Description
 */
@Component
public class NClient {
    private final Logger log = LoggerFactory.getLogger(NClient.class);
    private final static String APP_TOKEN = "nn:token";
    private final NProperties properties;
    private final RedisTemplate redisTemplate;
    private String orderNo;

    /**
     * 开启重试机制
     */
    private int retryTimes=3;

    public NClient(NProperties properties, RedisTemplate redisTemplate) {
        this.properties = properties;
        this.redisTemplate = redisTemplate;
    }

    /**
     *
     * @param method
     * @param payload
     * @return
     * @param <T>
     */
    public <T> T client(NMethod method, Validator payload) {
       return client(method, payload, properties);
    }

    /**
     *
     * @param method
     * @param payload
     * @return
     * @param <T>
     */
    public <T> T client(NMethod method, Validator payload, NProperties properties) {
        return client(method, payload, properties, null);
    }

    /**
     *
     * @param method
     * @param payload
     * @return
     * @param <T>
     */
    public <T> T client(NMethod method, Validator payload, NProperties properties, NCallback callback) {
        if (properties.isUseStrict()) payload.valid();
        fillData(method, payload);
        return clientJson(method, JsonMapper.toJson(payload), properties, callback);
    }

    /**
     * 开票回调
     * @param callback
     */
    public void callback(Callback callback) {
        callback(callback, null, null);
    }

    /**
     * 开票回调
     * @param callback 回调函数
     * @param nCallback 回调日志函数
     * @param payload 回调内容
     */
    public void callback(Callback callback, NCallback nCallback, org.nuonuo.sdk.model.callback.Callback payload) {
        NNExEnum result = NNExEnum.C0000;
        try {
            Callback.execute(callback);
        } catch (Exception e) {
            log.error("诺诺开票回调失败", e);
            result = NNExEnum.C9999;
        } finally {
            Response response = new Response(result);
            log(response, payload, nCallback);
            Servlets.write(response);
        }
    }

    /**
     *
     * @param method
     * @param payload
     * @return
     * @param <T>
     */
    public <T> T clientJson(NMethod method, String payload) {
        return clientJson(method, payload, this.properties);
    }

    /**
     *
     * @param method
     * @param payload
     * @return
     * @param <T>
     */
    public <T> T clientJson(NMethod method, String payload, NProperties properties) {
        return clientJson(method, payload, properties, null);
    }

    /**
     *
     * @param method
     * @param payload
     * @return
     * @param <T>
     */
    public <T> T clientJson(NMethod method, String payload, NProperties properties, NCallback callback) {
        properties.valid();
        NSDK sdk = NSDK.getIntance();
        String token = getToken();
        String senid = UUID.randomUUID().toString().replace("-", ""); // 唯一标识，32位随机码，无需修改，保持默认即可
        NResponse response = sdk.sendPostSyncRequest(properties.getUrl(), senid, properties.getAppKey(), properties.getAppSecret(), token, properties.getSalerTaxNum(), method.getValue(), payload);
        log(method, response, callback);
        NN<T> nn;
        switch (method) {
            case GET_PDF:
            case QUERY_INVOICE_RED_CONFIRM:
                NN n = JsonMapper.parse(response.getBody(), method.getClazz());
                if (JsonMapper.isNotEmpty(n.getCode()) && !NNExEnum.E0000.getValue().equals(n.getCode()))  throw new ValidationException(n.getDescribe());
                return (T) n;
            case QUERY_INVOICE_RESULT:
                nn = JsonMapper.parseList(response.getBody(), NN.class, method.getClazz());
                break;
            default:
                nn = JsonMapper.parse(response.getBody(), NN.class, method.getClazz());
        }

        if (!NNExEnum.E0000.getValue().equals(nn.getCode())) throw new ValidationException(properties.getWarmReminder()+nn.getDescribe());
        if (!method.equals(NMethod.REQUEST_BILLING_NEW)) return nn.getResult();
        return properties.isAutoGetInvoice() ? getInvoice(((BillingNewRes) nn.getResult()).getInvoiceSerialNum(), properties, callback) : (T) new InvoiceRes(){{
            setSerialNo(((BillingNewRes) nn.getResult()).getInvoiceSerialNum());
            setOrderNo(orderNo);
            setStatus("20");
            setStatusMsg("开票中");
        }};
    }

    /**
     * 发票详情获取
     * @param invoiceSerial
     * @param callback
     * @return
     * @param <T>
     */
    public <T> T getInvoice(String invoiceSerial, NProperties properties, NCallback callback) {
        return getInvoice(invoiceSerial, null, properties, callback);
    }

    /**
     * 发票详情获取
     * @param invoiceSerial
     * @param orderNo
     * @param callback
     * @return
     * @param <T>
     */
    public <T> T getInvoice(String invoiceSerial, String orderNo, NProperties properties, NCallback callback) {
        if (JsonMapper.isEmpty(invoiceSerial) && JsonMapper.isEmpty(orderNo)) throw new ValidationException("发票流水号和订单编号两字段二选一");
        return (T) getInvoices(new InvoiceReq(){{
            if (JsonMapper.isNotEmpty(invoiceSerial)) setSerialNos(new ArrayList<String>(){{add(invoiceSerial);}});
            if (JsonMapper.isNotEmpty(orderNo)) setOrderNos(new ArrayList<String>(){{add(orderNo);}});

        }}, callback).get(0);
    }

    /**
     * 发票详情
     * @param req
     * @param callback
     * @return,
     * @param <T>
     */
    public <T> List<T> getInvoices(InvoiceReq req, NCallback callback) {
        return client(NMethod.QUERY_INVOICE_RESULT, req, this.properties, callback);
    }

    /**
     * 发票详情
     * @param req
     * @param callback
     * @return
     * @param <T>
     */
    public <T> List<T> getInvoices(InvoiceReq req, NProperties properties, NCallback callback) {
        return client(NMethod.QUERY_INVOICE_RESULT, req, properties, callback);
    }

    /**
     *
     * @param method
     * @param payload
     */
    private void fillData(NMethod method, Validator payload) {
        switch (method) {
            case REQUEST_BILLING_NEW: // 开票
                BillingNewReq req = (BillingNewReq) payload;
                InvoiceOrder order = req.getOrder();

                order.setCallBackUrl(ifnull(order.getCallBackUrl(), properties.getCallback()));
                order.setClerk(ifnull(order.getClerk(), properties.getClerk()));
                order.setChecker(ifnull(order.getChecker(), properties.getChecker()));
                order.setPayee(ifnull(order.getPayee(), properties.getPayee()));
                order.setSalerTaxNum(ifnull(order.getSalerTaxNum(), properties.getSalerTaxNum()));
                order.setSalerAccount(ifnull(order.getSalerAccount(), properties.getSalerAccount()));
                order.setSalerTel(ifnull(order.getSalerTel(), properties.getSalerTel()));
                order.setSalerAddress(ifnull(order.getSalerAddress(), properties.getSalerAddress()));
                order.setShowCheckerType(properties.isShowChecker()?"1":"0");
                this.orderNo =  order.getOrderNo();
                break;
            case QUERY_INVOICE_RESULT: // 发票查询
                InvoiceReq invoiceReq = (InvoiceReq) payload;
                if (properties.isOfferInvoiceDetail())invoiceReq.setIsOfferInvoiceDetail("1");
                break;
            case INVOICE_RED_CONFIRM: // 红冲确认
                InvoiceRedReq red = (InvoiceRedReq) payload;
                red.setCallbackUrl(ifnull(red.getCallbackUrl(), properties.getCallback()));
                red.setSellerTaxNo(ifnull(red.getSellerTaxNo(), properties.getSalerTaxNum()));
                red.setSellerName(ifnull(red.getSellerName(), properties.getSalerName()));
                red.setCallbackUrl(ifnull(red.getCallbackUrl(), properties.getRedConfirmCallback(), properties.getCallback()));
                break;
            case FAST_INVOICE_RED: // 发票快捷冲红
                InvoiceFastRedReq redReq = (InvoiceFastRedReq) payload;
                redReq.setCallBackUrl(ifnull(redReq.getCallBackUrl(), properties.getFastRedCallback(), properties.getCallback()));
                redReq.setTaxNum(ifnull(redReq.getTaxNum(), properties.getSalerTaxNum()));
//                红字确认单编号,数电红票必传；且必须在备注中注明“被冲红蓝字数电票号码：YYYYYYYYY 红字发票信息确认单编号：XXXXXXXXX”字样，其中“Y”为数电蓝票的号码，“X”对应的红字确认单编号（由接口自动拼接）

                this.orderNo = redReq.getOrderNo();
                break;
        }

    }

    private <T> T ifnull(T ...value) {
        for(int i = 0, j = value.length; i < j; i++) {
            if (JsonMapper.isNotEmpty(value[i])) return value[i];
        }
        return null;
    }

    /**
     * 访问令牌
     * @return
     */
    private String getToken() {
        String token = (String) redisTemplate.opsForValue().get(APP_TOKEN);
        if (token != null) return token;

        String data = ModuleAuth.getMerchantToken(properties.getAppKey(), properties.getAppSecret(), null, ModuleAuth.TOKEN_URL);

        AuthTokenRes res = JsonMapper.parse(data, AuthTokenRes.class);
        if (res.getAccessToken() == null) {
            log.warn("获取Token失败，原因：{}", res.getErrorDescription());
            throw new ValidationException("获取Token失败，原因："+res.getErrorDescription());
        }

        if (res.getExpiresIn()==-1) redisTemplate.opsForValue().set(APP_TOKEN, res.getAccessToken());
        else redisTemplate.opsForValue().set(APP_TOKEN, res.getAccessToken(), res.getExpiresIn()-60, TimeUnit.SECONDS);

        return res.getAccessToken();
    }

    private void log(NMethod method, NResponse response, NCallback callback) {
        if (properties.isCloseLog())return;
        NLog nLog = new NLog();
        nLog.setUrl(response.getUrl());
        nLog.setName(method.getLabel());
        nLog.setMethod(method.getValue());
        nLog.setHeaders(response.getHeaders());
        nLog.setParams(response.getParams());
        nLog.setPayload(response.getPayload());
        nLog.setResponse(response.getBody());
        if (properties.isRetLog()) NCallback.execute(callback, nLog);
        if (properties.isPrintLog()) log.info("诺诺开票平台请求：\n\t接口：{}\n\t请求方法：[{}/{}]\n\t请求头：{}\n\t请求参数：{}\n\t请求体：{}\n\t响应体：{}", nLog.getUrl(), nLog.getMethod(), method.getLabel(), JsonMapper.toJson(nLog.getHeaders()), JsonMapper.toJson(response.getParams()), JsonMapper.toJson(response.getPayload()), response.getBody());
    }

    /**
     * 日志输出
     * @param response 响应内容
     * @param payload 回调参数
     * @param callback 日志函数
     */
    private void log(Response response, org.nuonuo.sdk.model.callback.Callback payload, NCallback callback) {
        if (properties.isCloseLog())return;
        NMethod method = NMethod.getMethod(payload.getOperater());
        String name = method == null ? "发票回调" : method.name();
        if (properties.isRetLog()) NCallback.execute(callback, new NLog(){{
            setName(name);
            setUrl(properties.getCallback());
            setMethod(payload.getOperater());
            setPayload(payload);
            setResponse(response);
        }});
        if (properties.isPrintLog()) log.info("诺诺开票平台回调请求：\n\t接口：{}\n\t请求方法：[{}/{}]\n\t请求体：{}\n\t响应体：{}", properties.getCallback(), payload.getOperater(), name, JsonMapper.toJson(payload), JsonMapper.toJson(response));
    }
}
